Actividad 5#
Valentina Cabrera
Librerias#
import pandas as pd
from IPython.display import display
import seaborn as sns
import matplotlib.pyplot as plt
import warnings
import numpy as np
import plotly.express as px
import missingno as msno
import plotly.graph_objects as go
import geopandas as gpd
from urllib.request import urlopen
from numpy import mean
from tabulate import tabulate
from scipy import stats
import folium
from folium import Choropleth, Popup, Tooltip
warnings.filterwarnings('ignore')
Accidentalidad en Barranquilla.#
Base de datos#
data = pd.read_csv('C:/Users/valec/Downloads/Escritorio/Accidentalidad_en_Barranquilla_20240826.csv')
display(data.head(10).style.set_caption("Base de datos: Accidentalidad en Barranquilla"))
| FECHA_ACCIDENTE | HORA_ACCIDENTE | GRAVEDAD_ACCIDENTE | CLASE_ACCIDENTE | SITIO_EXACTO_ACCIDENTE | CANT_HERIDOS_EN _SITIO_ACCIDENTE | CANT_MUERTOS_EN _SITIO_ACCIDENTE | CANTIDAD_ACCIDENTES | AÑO_ACCIDENTE | MES_ACCIDENTE | DIA_ACCIDENTE | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2018-01-01T00:00:00.000 | 01:30:00:am | Con heridos | Atropello | CL 87 9H 24 | 1.000000 | nan | 1 | 2018 | January | Mon |
| 1 | 2018-01-01T00:00:00.000 | 02:00:00:pm | Solo daños | Choque | CL 110 CR 46 | nan | nan | 1 | 2018 | January | Mon |
| 2 | 2018-01-01T00:00:00.000 | 04:00:00:am | Solo daños | Choque | AV CIRCUNVALAR CR 9G | nan | nan | 1 | 2018 | January | Mon |
| 3 | 2018-01-01T00:00:00.000 | 04:30:00:am | Solo daños | Choque | CLLE 72 CRA 29 | nan | nan | 1 | 2018 | January | Mon |
| 4 | 2018-01-01T00:00:00.000 | 05:20:00:pm | Solo daños | Choque | VIA 40 CALLE 75 | nan | nan | 1 | 2018 | January | Mon |
| 5 | 2018-01-01T00:00:00.000 | 06:00:00:pm | Con heridos | Choque | CR 8 CL 41 | 3.000000 | nan | 1 | 2018 | January | Mon |
| 6 | 2018-01-01T00:00:00.000 | 12:50:00:am | Con heridos | Atropello | CLLE 119B CRA 11B | 1.000000 | nan | 1 | 2018 | January | Mon |
| 7 | 2018-01-02T00:00:00.000 | 02:30:00:pm | Solo daños | Choque | CARRERA 25 37-42 | nan | nan | 1 | 2018 | January | Tue |
| 8 | 2018-01-02T00:00:00.000 | 03:00:00:pm | Solo daños | Choque | CR 51B 1D 35 | nan | nan | 1 | 2018 | January | Tue |
| 9 | 2018-01-02T00:00:00.000 | 03:45:00:pm | Solo daños | Choque | VIA 40 CLLE 85 | nan | nan | 1 | 2018 | January | Tue |
Análisis: características de la base de datos#
Esta base de datos, proporcionada por la Alcaldía Distrital de Barranquilla, contiene información relacionada con los accidentes de tránsito que han ocurrido en la ciudad desde 2018 hasta el 30 de junio de 2024, según los informes policiales de accidente de tránstito (IPAT). Cada una de las columnas, proporciona información específica sobre cada evento, como por ejemplo la fecha y lugar dónde ocurrió, la cantidad de heridos, gravedad del suceso, etc.
En este caso, contamos con 25610 observaciones, las cuales representan cada accidente, y con 11 variables. Veamos ahora, la información relacionada con el nombre y tipo de las variables.
df.info()
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[3], line 1
----> 1 df.info()
NameError: name 'df' is not defined
Al ver la respuesta proporcionada por df.info(), observamos que indica que hay 4 variables de tipo numérica, sin embargo, según el sitio web que proporcionó la base de datos, la variable AÑO_ACCIDENTE, es tipo factor/caracter. Por tanto,
data['AÑO_ACCIDENTE'] = data['AÑO_ACCIDENTE'].astype('object')
Por tanto, podemos ahora afirmar que, de las 11 variables, 3 de ellas, CANT_HERIDOS_EN_SITIO_ACCIDENTE, CANT_MUERTOS_EN_SITIO_ACCIDENTE y CANTIDAD_ACCIDENTES, son numéricas y las 8 restantes son categóricas.
Análisis: variable según su tipo#
Numéricas:#
En primer lugar, veamos las principales estadísticas descriptivas de cada variable numérica.
data.describe()
| CANT_HERIDOS_EN _SITIO_ACCIDENTE | CANT_MUERTOS_EN _SITIO_ACCIDENTE | CANTIDAD_ACCIDENTES | |
|---|---|---|---|
| count | 9984.000000 | 252.000000 | 25610.000000 |
| mean | 1.471655 | 1.035714 | 1.000195 |
| std | 1.156751 | 0.185946 | 0.013972 |
| min | 1.000000 | 1.000000 | 1.000000 |
| 25% | 1.000000 | 1.000000 | 1.000000 |
| 50% | 1.000000 | 1.000000 | 1.000000 |
| 75% | 2.000000 | 1.000000 | 1.000000 |
| max | 42.000000 | 2.000000 | 2.000000 |
Ahora, grafiquemos cada una de ellas por separado para analizarlas.
CANT_HERIDOS_EN _SITIO_ACCIDENTE#
sns.boxplot(x=data['CANT_HERIDOS_EN _SITIO_ACCIDENTE'], color='lightblue')
plt.title('Boxplot de la cantidad de heridos')
plt.xlabel('Cantidad de Heridos')
plt.show()
Lo que podemos mencionar sobre esta variable es que el promedio de personas heridas es aproximadamente 1 por accidente, sin embargo, se nota que hay casos en donde esto varió y hubo un alto número de personas afectadas, como es visible en la gráfica la cual cuenta con una gran cantidad de datos atípicos.
CANT_MUERTOS_EN _SITIO_ACCIDENTE#
sns.boxplot(x=data['CANT_MUERTOS_EN _SITIO_ACCIDENTE'], color='lightblue')
plt.title('Boxplot de la cantidad de muertos')
plt.xlabel('Cantidad de muertos')
plt.show()
Lo que podemos mencionar sobre esta variable es que el promedio de personas muertas es 1 por accidente, sin embargo, se nota que estos valores varían entre 1 y 2 dada la existencia del dato atípico.
CANTIDAD_ACCIDENTES#
sns.boxplot(x=data['CANTIDAD_ACCIDENTES'], color='lightblue')
plt.title('Boxplot de la cantidad de accidentes')
plt.xlabel('Cantidad de accidentes')
plt.show()
Lo que podemos mencionar sobre esta variable es que el promedio de accidentes por hora específica en cada fecha es 1, sin embargo, se nota que estos valores varían entre 1 y 2 dada la existencia del dato atípico.
Correlación#
Analicemos si existe alguna correlación entre estas.
data_num = data[['CANT_HERIDOS_EN _SITIO_ACCIDENTE', 'CANT_MUERTOS_EN _SITIO_ACCIDENTE','CANTIDAD_ACCIDENTES']]
matriz_correlacion = data_num.corr()
plt.figure(figsize=(12, 8)) # Configuración del tamaño de la figura
sns.heatmap(matriz_correlacion, annot = True, cmap = 'Blues', fmt = '.2f', vmin = -1, vmax = 1)
plt.title('Mapa de Calor de Correlaciones')
plt.show()
Esto quiere decir que no hay ningun par de variables que cuente con una correlación significativa, por tanto, podemos afirmar que el comportamiento u ocurrencia de cada una de ellas es independiente a las demás.
Categóricas#
En primer lugar, veamos una descripción resumida de cada una.
data.describe(include = object)
| FECHA_ACCIDENTE | HORA_ACCIDENTE | GRAVEDAD_ACCIDENTE | CLASE_ACCIDENTE | SITIO_EXACTO_ACCIDENTE | AÑO_ACCIDENTE | MES_ACCIDENTE | DIA_ACCIDENTE | |
|---|---|---|---|---|---|---|---|---|
| count | 25610 | 25610 | 25610 | 25610 | 25610 | 25610 | 25610 | 25610 |
| unique | 2357 | 871 | 3 | 6 | 15365 | 7 | 12 | 7 |
| top | 2018-06-08T00:00:00.000 | 04:00:00:pm | Solo daños | Choque | CL 110 CR 9G | 2018 | February | Tue |
| freq | 34 | 408 | 15457 | 23819 | 77 | 5898 | 2477 | 4009 |
Ahora, vamos a analizarlas con más profundidad.
FECHA_ACCIDENTE#
frec_fecha = data['FECHA_ACCIDENTE'].value_counts().sort_values(ascending=False)
frec_fecha
FECHA_ACCIDENTE
2018-06-08T00:00:00.000 34
2018-11-16T00:00:00.000 33
2019-05-22T00:00:00.000 31
2019-10-05T00:00:00.000 30
2020-02-13T00:00:00.000 29
..
2020-07-05T00:00:00.000 1
2024-02-07T00:00:00.000 1
2020-08-16T00:00:00.000 1
2020-08-10T00:00:00.000 1
2020-08-09T00:00:00.000 1
Name: count, Length: 2357, dtype: int64
Es decir, el día que hubo mayor cantidad de accidentes fue el 8 de junio del año 2018, con 34 incidentes.
HORA_ACCIDENTE#
frec_hora = data['HORA_ACCIDENTE'].value_counts().sort_values(ascending=False)
frec_hora
HORA_ACCIDENTE
03:00:00:pm 408
04:00:00:pm 408
12:30:00:pm 387
05:00:00:pm 384
08:00:00:am 384
...
04:43:00:pm 1
01:06:00:am 1
01:56:00:pm 1
05:23:00:pm 1
07:24:00:pm 1
Name: count, Length: 871, dtype: int64
Es decir que, según los datos recolectados, los accidentes tendían a ocurrir en su mayoría en horas de la tarde, más específicamente a las 3pm y 4pm.
GRAVEDAD_ACCIDENTE#
frec_gravedad = data['GRAVEDAD_ACCIDENTE'].value_counts()
frec_gravedad
GRAVEDAD_ACCIDENTE
Solo daños 15457
Con heridos 9901
Con muertos 252
Name: count, dtype: int64
ax = sns.catplot(data = data, x = 'GRAVEDAD_ACCIDENTE', aspect = 1.5, kind = "count", color = "lightblue")
ax.set_axis_labels('Gravedad del accidente', 'Número de accidentes')
ax.fig.suptitle('Frecuencia del tipo de gravedad de accidente', fontsize = 16)
sns.set_style("whitegrid")
plt.subplots_adjust(top=0.9) # Ajusta el espacio para el título
plt.grid(True, which = 'both', linestyle = '--', linewidth = 0.5)
plt.show()
A partir de este diagrama de barras y las frecuencias calculadas, se puede afirmar que la mayoría de los accidentes no tuvieron consecuencias graves para las personas, ya que en su mayoría solo se reportaron daños materiales. Además, el número de fallecidos es significativamente menor en comparación con los otros dos casos: Con heridos y Solo daños.
CLASE_ACCIDENTE#
data['CLASE_ACCIDENTE'].value_counts()
CLASE_ACCIDENTE
Choque 23819
Atropello 1344
Caida Ocupante 194
Otro 123
Volcamiento 117
Incendio 13
Name: count, dtype: int64
ax = sns.catplot(data = data, x = 'CLASE_ACCIDENTE', aspect = 1.5, kind = "count", color = "lightblue")
ax.set_axis_labels('Clase de accidente', 'Número de accidentes')
ax.fig.suptitle('Frecuencia de cada clase de accidente', fontsize=16)
sns.set_style("whitegrid")
plt.subplots_adjust(top = 0.9)
plt.grid(True, which ='both', linestyle = '--', linewidth = 0.5)
plt.show()
A partir de esta gráfica y las frecuencias calculadas, se puede evidenciar con claridad que la mayor cantidad de accidentes se debe, de manera significativa, al tipo Choque. El resto de las clases de accidentes se distribuye casi de manera proporcional entre las demás categorías, considerando la frecuencia de Atropello ligeramente mayor.
año_accidente#
data['AÑO_ACCIDENTE'].value_counts()
AÑO_ACCIDENTE
2018 5898
2019 5645
2021 4700
2022 3683
2020 3281
2023 1662
2024 741
Name: count, dtype: int64
ax = sns.catplot(data = data, x = 'AÑO_ACCIDENTE', aspect = 1.5, kind = "count", color = "lightblue")
ax.set_axis_labels('Año de ocurrencia', 'Número de accidentes')
ax.fig.suptitle('Frecuencia de año de ocurrencia de accidente', fontsize=16)
sns.set_style("whitegrid")
plt.subplots_adjust(top = 0.9)
plt.grid(True, which ='both', linestyle = '--', linewidth = 0.5)
plt.show()
Se puede observar que desde el año 2018 hasta el año actual, la tendencia de ocurrencia de accidentes ha disminuido con una variablidad un tanto significativa. El año con el mayor número de accidentes registrados, 5898, fue 2018, mientras que 2024 tuvo la menor frecuencia, 741. A su vez, siguiendo la idea del principio, se nota que a partir del 2021, se mantiene un patrón de descenso continuo en la frecuencia de accidentes en la ciudad de Barranquilla.
MES_ACCIDENTE#
data['MES_ACCIDENTE'].value_counts()
MES_ACCIDENTE
February 2477
March 2446
January 2349
December 2189
May 2121
June 2103
October 2090
April 2010
November 1995
September 1980
July 1932
August 1918
Name: count, dtype: int64
ax = sns.catplot(data = data, x = 'MES_ACCIDENTE', aspect = 1.5, kind = "count", color = "lightblue")
ax.set_axis_labels('Mes de ocurrencia', 'Número de accidentes')
ax.fig.suptitle('Frecuencia del mes de ocurrencia del accidente', fontsize=16)
sns.set_style("whitegrid")
plt.subplots_adjust(top = 0.9)
plt.xticks(rotation = 45, ha = 'right')
plt.grid(True, which = 'both', linestyle = '--', linewidth = 0.5)
plt.show()
Se puede observar que la cantidad de accidentes por mes es bastante similar, con valores cercanos a aproximadamente 2000. El mes con el mayor número de accidentes acumulados es febrero, con 2477 accidentes, mientras que el mes con el menor número es agosto, con 1918 registros.
DIA_ACCIDENTE#
data['DIA_ACCIDENTE'].value_counts()
DIA_ACCIDENTE
Tue 4009
Fri 3920
Wed 3839
Mon 3774
Thu 3756
Sat 3735
Sun 2577
Name: count, dtype: int64
ax = sns.catplot(data = data, x = 'DIA_ACCIDENTE', aspect = 1.5, kind = "count", color = "lightblue")
ax.set_axis_labels('Día de ocurrencia', 'Número de accidentes')
ax.fig.suptitle('Frecuencia de día de ocurrencia del accidente', fontsize = 16)
sns.set_style("whitegrid")
plt.subplots_adjust(top = 0.9)
plt.grid(True, which = 'both', linestyle = '--', linewidth = 0.5)
plt.show()
En este gráfico, se puede observar que la frecuencia de accidentes es bastante parecida entre la mayoría de los días de la semana, con la excepción notable del domingo, que tiene una frecuencia visiblemente menor. Según los registros de este estudio, el día con la mayor cantidad de accidentes es el martes, con 4009 incidentes, mientras que, como ya se mencionó, el domingo tiene la menor cantidad, con 2577 accidentes.
Filtrado de la base de datos#
Para esta sección, vamos a filtrar la base de datos con diferentes casos y poder así explorar los datos.
1. Accidentes de tipo choque ocurridos en los años 2018 y 2019#
Haremos un breve análisis de este subconjunto usando las variables DIA_ACCIDENTE, GRAVEDAD_ACCIDENTE y MES_ACCIDENTE.
filtro1 = data[(data['CLASE_ACCIDENTE'] == "Choque") & (data['AÑO_ACCIDENTE'].isin([2018, 2019]))]
display(filtro1.head(10).style.set_caption("Base de datos: Filtro 1"))
| FECHA_ACCIDENTE | HORA_ACCIDENTE | GRAVEDAD_ACCIDENTE | CLASE_ACCIDENTE | SITIO_EXACTO_ACCIDENTE | CANT_HERIDOS_EN _SITIO_ACCIDENTE | CANT_MUERTOS_EN _SITIO_ACCIDENTE | CANTIDAD_ACCIDENTES | AÑO_ACCIDENTE | MES_ACCIDENTE | DIA_ACCIDENTE | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 2018-01-01T00:00:00.000 | 02:00:00:pm | Solo daños | Choque | CL 110 CR 46 | nan | nan | 1 | 2018 | January | Mon |
| 2 | 2018-01-01T00:00:00.000 | 04:00:00:am | Solo daños | Choque | AV CIRCUNVALAR CR 9G | nan | nan | 1 | 2018 | January | Mon |
| 3 | 2018-01-01T00:00:00.000 | 04:30:00:am | Solo daños | Choque | CLLE 72 CRA 29 | nan | nan | 1 | 2018 | January | Mon |
| 4 | 2018-01-01T00:00:00.000 | 05:20:00:pm | Solo daños | Choque | VIA 40 CALLE 75 | nan | nan | 1 | 2018 | January | Mon |
| 5 | 2018-01-01T00:00:00.000 | 06:00:00:pm | Con heridos | Choque | CR 8 CL 41 | 3.000000 | nan | 1 | 2018 | January | Mon |
| 7 | 2018-01-02T00:00:00.000 | 02:30:00:pm | Solo daños | Choque | CARRERA 25 37-42 | nan | nan | 1 | 2018 | January | Tue |
| 8 | 2018-01-02T00:00:00.000 | 03:00:00:pm | Solo daños | Choque | CR 51B 1D 35 | nan | nan | 1 | 2018 | January | Tue |
| 9 | 2018-01-02T00:00:00.000 | 03:45:00:pm | Solo daños | Choque | VIA 40 CLLE 85 | nan | nan | 1 | 2018 | January | Tue |
| 10 | 2018-01-02T00:00:00.000 | 04:10:00:pm | Solo daños | Choque | CL 19 CR 2C | nan | nan | 1 | 2018 | January | Tue |
| 11 | 2018-01-02T00:00:00.000 | 05:30:00:pm | Solo daños | Choque | VIA 40 CL 77B | nan | nan | 1 | 2018 | January | Tue |
frec_diafiltro1 = filtro1['DIA_ACCIDENTE'].value_counts()
frec_diafiltro1
DIA_ACCIDENTE
Tue 1727
Fri 1680
Wed 1668
Thu 1626
Mon 1602
Sat 1577
Sun 1002
Name: count, dtype: int64
Al analizar este nuevo subconjunto con respecto a los días de la semana, podemos observar que el viernes sigue siendo el día con mayor cantidad de accidentes, en este caso, solo de tipo Choque.
frec_gravedadfiltro1 = filtro1['GRAVEDAD_ACCIDENTE'].value_counts()
frec_gravedadfiltro1
GRAVEDAD_ACCIDENTE
Solo daños 8111
Con heridos 2725
Con muertos 46
Name: count, dtype: int64
Durante los años 2018 y 2019, los accidentes clasificados como Choque resultaron mayoritariamente en daños materiales. Además, es importante destacar que la frecuencia de Con muertos en estos accidentes fue significativamente menor en comparación con el número de Con heridos y Solo daños.
frec_mesfiltro1 = filtro1['MES_ACCIDENTE'].value_counts()
frec_mesfiltro1
MES_ACCIDENTE
September 962
April 940
December 931
March 930
August 919
October 916
May 911
November 909
January 885
February 874
July 856
June 849
Name: count, dtype: int64
Podemos observar que los accidentes de tipo Choque durante los años 2018 y 2019 se distribuyeron de manera casi uniforme a lo largo de los 12 meses. La cantidad de incidentes por mes se aproxima a los 900. En comparación con la base de datos original, el mes con la menor cantidad de choques acumulados es junio, con 849 incidentes, mientras que el mes con la mayor frecuencia es septiembre, con 962 incidentes.
2. Accidentes ocurridos los días sábado y domingo en el primer trimestre de cada año sin muertos#
Haremos un breve análisis de este subconjunto usando las variables AÑO_ACCIDENTE y CLASE_ACCIDENTE.
filtro2 = data[
(data['DIA_ACCIDENTE'].isin(["Sat", "Sun"])) &
(data['MES_ACCIDENTE'].isin(["January", "February", "March"])) &
(data['GRAVEDAD_ACCIDENTE'].isin(["Con heridos", "Solo daños"]))
]
display(filtro2.head(10).style.set_caption("Base de datos: Filtro 2"))
| FECHA_ACCIDENTE | HORA_ACCIDENTE | GRAVEDAD_ACCIDENTE | CLASE_ACCIDENTE | SITIO_EXACTO_ACCIDENTE | CANT_HERIDOS_EN _SITIO_ACCIDENTE | CANT_MUERTOS_EN _SITIO_ACCIDENTE | CANTIDAD_ACCIDENTES | AÑO_ACCIDENTE | MES_ACCIDENTE | DIA_ACCIDENTE | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 58 | 2018-01-06T00:00:00.000 | 02:25:00:pm | Con heridos | Caida Ocupante | AV CORDIALIDAD CON CIRCUNVALAR | 1.000000 | nan | 1 | 2018 | January | Sat |
| 59 | 2018-01-06T00:00:00.000 | 03:20:00:pm | Solo daños | Choque | CR 44 CL 42 | nan | nan | 1 | 2018 | January | Sat |
| 60 | 2018-01-06T00:00:00.000 | 05:30:00:pm | Solo daños | Choque | CALLE 70 CRA 48 | nan | nan | 1 | 2018 | January | Sat |
| 61 | 2018-01-06T00:00:00.000 | 07:50:00:pm | Solo daños | Choque | CLLE 84 CRA 42B1 | nan | nan | 1 | 2018 | January | Sat |
| 62 | 2018-01-06T00:00:00.000 | 09:30:00:am | Con heridos | Atropello | CLLE 110 CRA 6 | 2.000000 | nan | 1 | 2018 | January | Sat |
| 63 | 2018-01-06T00:00:00.000 | 10:15:00:am | Solo daños | Choque | VIA 40 CR 46 | nan | nan | 1 | 2018 | January | Sat |
| 64 | 2018-01-06T00:00:00.000 | 10:55:00:am | Solo daños | Choque | CRA 46 CLLE 76 | nan | nan | 1 | 2018 | January | Sat |
| 65 | 2018-01-06T00:00:00.000 | 11:25:00:am | Con heridos | Choque | CLLE 68 CRA 60 | 1.000000 | nan | 1 | 2018 | January | Sat |
| 66 | 2018-01-06T00:00:00.000 | 11:30:00:am | Con heridos | Choque | CRA 51B CALLE 90 | 1.000000 | nan | 1 | 2018 | January | Sat |
| 67 | 2018-01-06T00:00:00.000 | 11:30:00:am | Solo daños | Choque | CR 42F 75B 124 | nan | nan | 1 | 2018 | January | Sat |
frec_añofiltro2 = filtro2['AÑO_ACCIDENTE'].value_counts()
frec_añofiltro2
AÑO_ACCIDENTE
2019 346
2018 344
2022 292
2020 287
2021 275
2023 107
2024 105
Name: count, dtype: int64
En este subconjunto de datos, se puede observar un comportamiento similar al de la base de datos principal, con una ligera tendencia a la disminución de accidentes a lo largo de los años. Sin embargo, se destaca que en 2019 y 2022, la frecuencia de accidentes es ligeramente superior en comparación con el año anterior. El año con la mayor cantidad de accidentes, según los filtros establecidos, fue 2019, con 346 registros, mientras que el año con la menor cantidad fue 2024, con 105 registros.
frec_clasefiltro2 = filtro2['CLASE_ACCIDENTE'].value_counts()
frec_clasefiltro2
CLASE_ACCIDENTE
Choque 1619
Atropello 107
Caida Ocupante 13
Otro 8
Volcamiento 7
Incendio 2
Name: count, dtype: int64
Para este resultado, se puede mencionar que la cantidad de accidentes de tipo Choque, según los filtros establecidos, es significativamente mayor en comparación con las demás clases de accidente. La cantidad de accidentes de esta clase es de 1619, mientras que la clase con la menor frecuencia es Incendio, con solo 2 registros. Este subconjunto, analizado desde la variable de clase_accidente, refleja el mismo comportamiento que la base de datos original, ya que la distribución de los accidentes en otras clases es relativamente igual, siendo la frecuencia de Atropello ligeramente superior.
3. Accidentes ocurridos entre 2019 y 2021 en febrero#
Haremos un breve análisis de este subconjunto usando las variables DIA_ACCIDENTE, GRAVEDAD_ACCIDENTE y CLASE_ACCIDENTE.
filtro3 = data[
(data['AÑO_ACCIDENTE'].isin([2019, 2020, 2021])) &
(data['MES_ACCIDENTE'] == "February")
]
display(filtro3.head(10).style.set_caption("Base de datos: Filtro 3"))
| FECHA_ACCIDENTE | HORA_ACCIDENTE | GRAVEDAD_ACCIDENTE | CLASE_ACCIDENTE | SITIO_EXACTO_ACCIDENTE | CANT_HERIDOS_EN _SITIO_ACCIDENTE | CANT_MUERTOS_EN _SITIO_ACCIDENTE | CANTIDAD_ACCIDENTES | AÑO_ACCIDENTE | MES_ACCIDENTE | DIA_ACCIDENTE | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 6379 | 2019-02-01T00:00:00.000 | 01:00:00:pm | Con heridos | Choque | CL 8 41 43 | 1.000000 | nan | 1 | 2019 | February | Fri |
| 6380 | 2019-02-01T00:00:00.000 | 01:00:00:pm | Solo daños | Choque | CL 45 10C 08 | nan | nan | 1 | 2019 | February | Fri |
| 6381 | 2019-02-01T00:00:00.000 | 01:10:00:pm | Solo daños | Choque | CL 90 53 45 | nan | nan | 1 | 2019 | February | Fri |
| 6382 | 2019-02-01T00:00:00.000 | 01:30:00:pm | Solo daños | Choque | VIA 40 CL 85 | nan | nan | 1 | 2019 | February | Fri |
| 6383 | 2019-02-01T00:00:00.000 | 02:00:00:pm | Solo daños | Choque | CL 19 CR 1 | nan | nan | 1 | 2019 | February | Fri |
| 6384 | 2019-02-01T00:00:00.000 | 02:50:00:pm | Con heridos | Choque | VIA 40 CL 80 | 1.000000 | nan | 1 | 2019 | February | Fri |
| 6385 | 2019-02-01T00:00:00.000 | 05:50:00:pm | Con heridos | Choque | CR 15 SUR 78 123 | 1.000000 | nan | 1 | 2019 | February | Fri |
| 6386 | 2019-02-01T00:00:00.000 | 06:00:00:pm | Con heridos | Otro | CR 11 CL 35 | 1.000000 | nan | 1 | 2019 | February | Fri |
| 6387 | 2019-02-01T00:00:00.000 | 06:30:00:am | Solo daños | Choque | CL 53D 21 36 | nan | nan | 1 | 2019 | February | Fri |
| 6388 | 2019-02-01T00:00:00.000 | 06:30:00:pm | Solo daños | Choque | CR 38 CL 31 | nan | nan | 1 | 2019 | February | Fri |
frec_diafiltro3 = filtro3['DIA_ACCIDENTE'].value_counts()
frec_diafiltro3
DIA_ACCIDENTE
Sat 213
Mon 203
Fri 196
Thu 182
Tue 179
Wed 179
Sun 118
Name: count, dtype: int64
Durante el mes de febrero en los años 2019, 2020 y 2021, se puede afirmar que el día con la mayor cantidad de accidentes fue el sábado, con 213 incidentes. Por otro lado, el domingo registró la menor cantidad de accidentes, con 118 registros, siendo este el único valor significativamente distante de los demás días, que se mantienen cerca de los 200 registros.
frec_gravedadfiltro3 = filtro3['GRAVEDAD_ACCIDENTE'].value_counts()
frec_gravedadfiltro3
GRAVEDAD_ACCIDENTE
Solo daños 894
Con heridos 372
Con muertos 4
Name: count, dtype: int64
Se puede decir que la mayor parte de los accidentes, según el filtro establecido, resultó en daños materiales, ya que la categoría Solo daños es la que cuenta con la mayor cantidad de incidentes, alcanzando 894 registros. Esto es significativamente más alto en comparación con las categorías Con heridos y Con muertos.
frec_clasefiltro3 = filtro3['CLASE_ACCIDENTE'].value_counts()
frec_clasefiltro3
CLASE_ACCIDENTE
Choque 1205
Atropello 52
Volcamiento 5
Otro 4
Caida Ocupante 4
Name: count, dtype: int64
Este subconjunto, al igual que los anteriormente analizados, muestra una alta frecuencia en la clase de accidente Choque, que es significativamente mayor en comparación con las demás clases. Además, se observa que, nuevamente, la categoría Atropello tiene una frecuencia ligeramente superior a las otras, mientras que las demás clases de accidente presentan una distribución de datos más uniforme.
4. Accidentes de tipo atropello ocurridos en noviembre y diciembre ocurridos a las 6 am y 6 pm#
Haremos un breve análisis de este subconjunto usando las variables DIA_ACCIDENTE y GRAVEDAD_ACCIDENTE.
filtro4 = data[
(data['CLASE_ACCIDENTE'] == "Atropello") &
(data['MES_ACCIDENTE'].isin(["November", "December"])) &
(data['HORA_ACCIDENTE'].isin(["06:00:00:am", "06:00:00:pm"]))
]
display(filtro4.head(10).style.set_caption("Base de datos: Filtro 4"))
| FECHA_ACCIDENTE | HORA_ACCIDENTE | GRAVEDAD_ACCIDENTE | CLASE_ACCIDENTE | SITIO_EXACTO_ACCIDENTE | CANT_HERIDOS_EN _SITIO_ACCIDENTE | CANT_MUERTOS_EN _SITIO_ACCIDENTE | CANTIDAD_ACCIDENTES | AÑO_ACCIDENTE | MES_ACCIDENTE | DIA_ACCIDENTE | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 5396 | 2018-12-02T00:00:00.000 | 06:00:00:am | Con heridos | Atropello | CALLE 45 CARRERA 15SUR | 1.000000 | nan | 1 | 2018 | December | Sun |
| 11126 | 2019-12-03T00:00:00.000 | 06:00:00:pm | Con heridos | Atropello | CALLE 86 CRA 21 | 2.000000 | nan | 1 | 2019 | December | Tue |
| 14572 | 2020-12-13T00:00:00.000 | 06:00:00:pm | Con heridos | Atropello | AV CIRCUNVALAR CR 25 | 1.000000 | nan | 1 | 2020 | December | Sun |
| 14631 | 2020-12-17T00:00:00.000 | 06:00:00:pm | Con heridos | Atropello | CR 38 CL 77 | 1.000000 | nan | 1 | 2020 | December | Thu |
| 24624 | 2023-11-02T00:00:00.000 | 06:00:00:pm | Con heridos | Atropello | AVENIDA MURILLO CARRERA 11 SUR | 1.000000 | nan | 1 | 2023 | November | Thu |
frec_diafiltro4 = filtro4['DIA_ACCIDENTE'].value_counts()
frec_diafiltro4
DIA_ACCIDENTE
Sun 2
Thu 2
Tue 1
Name: count, dtype: int64
En este caso, se puede observar que solo 5 registros cumplen con los filtros establecidos. De estos, 1 ocurrió un martes, 2 un jueves y 2 un domingo.
frec_gravedadfiltro4 = filtro4['GRAVEDAD_ACCIDENTE'].value_counts()
frec_gravedadfiltro4
GRAVEDAD_ACCIDENTE
Con heridos 5
Name: count, dtype: int64
En cuanto a la gravedad de los accidentes, ambos registros resultaron en la categoría Con heridos, lo que explica que su frecuencia sea de 5.
df_filtrado4= df[(df['HORA_ACCIDENTE'].between('06:00:00 am', '12:00:00 pm')) &
(df['CANTIDAD_ACCIDENTES'] == 2)]
df_filtrado4
| FECHA_ACCIDENTE | HORA_ACCIDENTE | GRAVEDAD_ACCIDENTE | CLASE_ACCIDENTE | SITIO_EXACTO_ACCIDENTE | CANT_HERIDOS_EN _SITIO_ACCIDENTE | CANT_MUERTOS_EN _SITIO_ACCIDENTE | CANTIDAD_ACCIDENTES | AÑO_ACCIDENTE | MES_ACCIDENTE | DIA_ACCIDENTE | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 2602 | 2018-06-09T00:00:00.000 | 09:35:00:am | Solo daños | Choque | CARRERA 38 CALLE 52 | NaN | NaN | 2 | 2018 | June | Sat |
| 12038 | 2020-02-06T00:00:00.000 | 09:00:00:am | Solo daños | Choque | AV CIRCUNVALAR CR 27 | NaN | NaN | 2 | 2020 | February | Thu |
| 12837 | 2020-05-02T00:00:00.000 | 07:40:00:pm | Solo daños | Choque | CR 46 87 27 | NaN | NaN | 2 | 2020 | May | Sat |
Este filtrado ha reducido el conjunto de datos a los accidentes ocurridos entre las 6:00 AM y las 12:00 PM que reportaron exactamente 2 accidentes en el sitio. En total se encontraron 3 observaciones que cumplen con estas condiciones.
df_filtrado5 = df[(df['DIA_ACCIDENTE'].isin(['Wed', 'Thu'])) &
(df['AÑO_ACCIDENTE'].isin([2020, 2021])) &
(df['HORA_ACCIDENTE'].between('06:00:00 am', '12:00:00 pm'))]
df_filtrado5.shape
(1296, 11)
df_filtrado5.head()
| FECHA_ACCIDENTE | HORA_ACCIDENTE | GRAVEDAD_ACCIDENTE | CLASE_ACCIDENTE | SITIO_EXACTO_ACCIDENTE | CANT_HERIDOS_EN _SITIO_ACCIDENTE | CANT_MUERTOS_EN _SITIO_ACCIDENTE | CANTIDAD_ACCIDENTES | AÑO_ACCIDENTE | MES_ACCIDENTE | DIA_ACCIDENTE | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 11546 | 2020-01-01T00:00:00.000 | 08:00:00:pm | Con heridos | Choque | CL 86 CR 9L | 1.00 | NaN | 1 | 2020 | January | Wed |
| 11547 | 2020-01-01T00:00:00.000 | 08:10:00:pm | Solo daños | Choque | CL 72 CR 44 25 | NaN | NaN | 1 | 2020 | January | Wed |
| 11548 | 2020-01-01T00:00:00.000 | 09:00:00:pm | Solo daños | Choque | CL 19 CR 2 | NaN | NaN | 1 | 2020 | January | Wed |
| 11549 | 2020-01-01T00:00:00.000 | 09:00:00:pm | Solo daños | Choque | CR 6 89 87 | NaN | NaN | 1 | 2020 | January | Wed |
| 11550 | 2020-01-01T00:00:00.000 | 09:30:00:pm | Solo daños | Choque | CL 72 CR 41 | NaN | NaN | 1 | 2020 | January | Wed |
Este filtrado ha reducido el conjunto de datos a los accidentes ocurridos los días miércoles y jueves durante los años 2020 y 2021, en la franja horaria de la mañana (entre las 6:00 AM y las 12:00 PM). En total se encontraron 1,296 observaciones que cumplen con estas condiciones.
5. Accidentes ocurridos a las 3, 4 y 5, de la mañana y tarde, en los años 2022 y 2023#
Haremos un breve análisis de este subconjunto usando las variables MES_ACCIDENTE, GRAVEDAD_ACCIDENTE y CLASE_ACCIDENTE.
filtro5 = data[
(data['HORA_ACCIDENTE'].isin(["03:00:00:am", "04:00:00:am", "05:00:00:am",
"03:00:00:pm", "04:00:00:pm", "05:00:00:pm"])) &
(data['AÑO_ACCIDENTE'].isin([2022, 2023]))
]
display(filtro5.head(10).style.set_caption("Filtro 5"))
filtro5_mañana = filtro5[
filtro5['HORA_ACCIDENTE'].isin(["03:00:00:am", "04:00:00:am", "05:00:00:am"])
]
filtro5_tarde = filtro5[
filtro5['HORA_ACCIDENTE'].isin(["03:00:00:pm", "04:00:00:pm", "05:00:00:pm"])
]
frec_mesfiltro5mañana = filtro5_mañana['MES_ACCIDENTE'].value_counts()
frec_mesfiltro5mañana
MES_ACCIDENTE
June 7
April 6
March 5
August 5
February 4
July 4
November 4
December 4
May 2
September 2
January 1
Name: count, dtype: int64
frec_mesfiltro5tarde = filtro5_tarde['MES_ACCIDENTE'].value_counts()
frec_mesfiltro5tarde
MES_ACCIDENTE
March 37
April 31
February 30
June 28
July 27
January 24
May 19
August 14
December 8
November 8
September 7
October 6
Name: count, dtype: int64
Podemos observar una clara diferencia en la cantidad de accidentes entre la mañana y la tarde por mes en los años estudiados. A partir de estos datos, se puede afirmar que, al comparar las horas seleccionadas, era más probable que ocurrieran accidentes durante la tarde que en la mañana. Esto se evidencia en que la frecuencia más alta en el grupo de horas de la mañana es de 7 accidentes, mientras que en la tarde alcanza los 37.
frec_gravedadfiltro5mañana = filtro5_mañana['GRAVEDAD_ACCIDENTE'].value_counts()
frec_gravedadfiltro5mañana
GRAVEDAD_ACCIDENTE
Con heridos 26
Solo daños 12
Con muertos 6
Name: count, dtype: int64
frec_gravedadfiltro5tarde = filtro5_tarde['GRAVEDAD_ACCIDENTE'].value_counts()
frec_gravedadfiltro5tarde
GRAVEDAD_ACCIDENTE
Solo daños 120
Con heridos 114
Con muertos 5
Name: count, dtype: int64
Nuevamente, se observa una gran diferencia en las frecuencias de los tipos de consecuencias, excluyendo la categoría Con muertos, ya que para los dos grupos son prácticamente iguales. Según los resultados, se puede inferir que la cantidad de accidentes es mayor en la tarde dentro del grupo de horas escogidas, dado que la cantidad de registros por categoría de gravedad es significativamente mayor en la tarde comparado con la mañana.
frec_clasefiltro5mañana = filtro5_mañana['CLASE_ACCIDENTE'].value_counts()
frec_clasefiltro5mañana
CLASE_ACCIDENTE
Choque 38
Atropello 3
Volcamiento 2
Otro 1
Name: count, dtype: int64
frec_gravedadfiltro5tarde = filtro5_tarde['CLASE_ACCIDENTE'].value_counts()
frec_gravedadfiltro5tarde
CLASE_ACCIDENTE
Choque 221
Atropello 16
Caida Ocupante 1
Otro 1
Name: count, dtype: int64
Para ambos grupos, los accidentes de tipo Choque fueron los más frecuentes. Sin embargo, al igual que en los análisis previos, la frecuencia es significativamente mayor en el grupo de horas de la tarde.
Identificación de valores NA#
Para identificar la existencia de los datos Not Available en la base de datos, vamos a contarlos y graficarlos.
Primeramente, calculemos cuántos hay por columna.
cantidad_na = data.isna().sum()
for columna in cantidad_na.index:
print(f"{columna}: {cantidad_na[columna]} NA's")
FECHA_ACCIDENTE: 0 NA's
HORA_ACCIDENTE: 0 NA's
GRAVEDAD_ACCIDENTE: 0 NA's
CLASE_ACCIDENTE: 0 NA's
SITIO_EXACTO_ACCIDENTE: 0 NA's
CANT_HERIDOS_EN _SITIO_ACCIDENTE: 15626 NA's
CANT_MUERTOS_EN _SITIO_ACCIDENTE: 25358 NA's
CANTIDAD_ACCIDENTES: 0 NA's
AÑO_ACCIDENTE: 0 NA's
MES_ACCIDENTE: 0 NA's
DIA_ACCIDENTE: 0 NA's
Es decir, que de las 11 variables, solo 2, CANT_HERIDOS_EN_ SITIO_ACCIDENTE y CANT_MUERTOS_EN_SITIO_ACCIDENTE, cuentan con datos faltantes. Nótese que, para la última mencionada, de las 25610 observaciones, 25318 son NA.
Ahora, veamos esto gráficamente.
porcentaje_NA = data.isna().mean() * 100
data_faltante = pd.DataFrame({'column': porcentaje_NA.index, 'percent_missing': porcentaje_NA.values})
data_faltante = data_faltante.sort_values(by='percent_missing')
plt.figure(figsize=(10, 8))
plt.barh(data_faltante['column'], data_faltante['percent_missing'], color='lightblue')
plt.xlabel('Porcentaje de datos faltantes')
plt.title('Porcentaje de datos faltantes por variable')
plt.show()
msno.bar(data, color = "lightblue", sort = 'ascending')
plt.title('Datos Faltantes por Columna', fontsize = 16, fontweight = 'bold')
plt.xlabel('Columnas', fontsize = 14)
plt.ylabel('Porcentaje de Datos Faltantes', fontsize = 14)
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
En la primera gráfica, se muestra el porcentaje de datos que faltan en cada variable. Por ejemplo, alrededor del 55% de los datos en la variable CANT_HERIDOS_EN _SITIO_ACCIDENTE son faltantes, y casi el 100% de los datos en CANT_MUERTOS_EN _SITIO_ACCIDENTE también son NA.
La segunda gráfica confirma esta información, mostrando cuántos datos hay en total por cada columna, lo que ayuda a entender mejor cuáles son las variables con más datos faltantes, tal como se calculó anteriormente.
Ahora, debido a que el porcentaje de NA’s de la variable CANT_MUERTOS_EN _SITIO_ACCIDENTE es notablemente significativo, se puede borrar la columna.
data2 = data.drop(columns=["CANT_MUERTOS_EN _SITIO_ACCIDENTE"])
display(data2.head(10))
| FECHA_ACCIDENTE | HORA_ACCIDENTE | GRAVEDAD_ACCIDENTE | CLASE_ACCIDENTE | SITIO_EXACTO_ACCIDENTE | CANT_HERIDOS_EN _SITIO_ACCIDENTE | CANTIDAD_ACCIDENTES | AÑO_ACCIDENTE | MES_ACCIDENTE | DIA_ACCIDENTE | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2018-01-01T00:00:00.000 | 01:30:00:am | Con heridos | Atropello | CL 87 9H 24 | 1.0 | 1 | 2018 | January | Mon |
| 1 | 2018-01-01T00:00:00.000 | 02:00:00:pm | Solo daños | Choque | CL 110 CR 46 | NaN | 1 | 2018 | January | Mon |
| 2 | 2018-01-01T00:00:00.000 | 04:00:00:am | Solo daños | Choque | AV CIRCUNVALAR CR 9G | NaN | 1 | 2018 | January | Mon |
| 3 | 2018-01-01T00:00:00.000 | 04:30:00:am | Solo daños | Choque | CLLE 72 CRA 29 | NaN | 1 | 2018 | January | Mon |
| 4 | 2018-01-01T00:00:00.000 | 05:20:00:pm | Solo daños | Choque | VIA 40 CALLE 75 | NaN | 1 | 2018 | January | Mon |
| 5 | 2018-01-01T00:00:00.000 | 06:00:00:pm | Con heridos | Choque | CR 8 CL 41 | 3.0 | 1 | 2018 | January | Mon |
| 6 | 2018-01-01T00:00:00.000 | 12:50:00:am | Con heridos | Atropello | CLLE 119B CRA 11B | 1.0 | 1 | 2018 | January | Mon |
| 7 | 2018-01-02T00:00:00.000 | 02:30:00:pm | Solo daños | Choque | CARRERA 25 37-42 | NaN | 1 | 2018 | January | Tue |
| 8 | 2018-01-02T00:00:00.000 | 03:00:00:pm | Solo daños | Choque | CR 51B 1D 35 | NaN | 1 | 2018 | January | Tue |
| 9 | 2018-01-02T00:00:00.000 | 03:45:00:pm | Solo daños | Choque | VIA 40 CLLE 85 | NaN | 1 | 2018 | January | Tue |
Por otra parte, CANT_HERIDOS_EN _SITIO_ACCIDENTE puede considerarse una variable de tipo factor debido a que sus valores no son continuos, por lo tanto, realizar una imputación sobre ella no es realmente necesario. Esto se debe a que los NA’s en esta variable reflejan adecuadamente la información sobre las consecuencias del accidente. De hecho, observamos que los NA’s aparecen cuando la variable GRAVEDAD_ACCIDENTE toma los valores Con muertos o Solo daños, lo que indica que la ausencia de datos en CANT_HERIDOS_EN _SITIO_ACCIDENTE va de la mano con GRAVEDAD_ACCIDENTE.
El resultado muestra las 5 primeras y 5 últimas filas de los datos filtrados, donde se observa que todas las entradas cumplen con estas condiciones: no tienen información sobre heridos (NaN en la columna CANT_HERIDOS_EN _SITIO_ACCIDENTE) y corresponden a accidentes clasificados como “Solo daños” o “Con muertos”.
La imputación de valores faltantes en esta variable no es realmente necesaria, ya que se observa que los NA’s en esta variable tienden a aparecer cuando la variable GRAVEDAD_ACCIDENTE toma los valores “Con muertos” o “Solo daños”, lo que indica una relación directa entre la gravedad del accidente y la ausencia de datos en la variable CANT_HERIDOS_EN _SITIO_ACCIDENTE
Detección de valores atípicos#
Dado que las variables numéricas en este conjunto de datos pueden ser vistas y trabajadas como factores, la detección de valores atípicos no es la estrategia más efectiva. En vez de buscar anomalías, se optará por calcular tablas de frecuencias para analizar cómo se distribuyen las diferentes categorías. Este método proporcionará una visión clara de la distribución de los valores en estas variables discretas, facilitando un análisis más pertinente y ajustado a la naturaleza de los datos.
CANT_HERIDOS_EN_SITIO_ACCIDENTES#
frec_heridos = data2['CANT_HERIDOS_EN _SITIO_ACCIDENTE'].value_counts().reset_index()
frec_heridos.columns = ['CANT_HERIDOS_EN _SITIO_ACCIDENTE', 'n'] #
frec_heridos = frec_heridos.sort_values(by='n', ascending=False)
table = go.Figure(data=[go.Table(
header=dict(values=list(frec_heridos.columns),
fill_color='lightblue',
align='left'),
cells=dict(values=[frec_heridos['CANT_HERIDOS_EN _SITIO_ACCIDENTE'], frec_heridos['n']],
fill_color='lightgrey',
align='left'))
])
table.show()
Es evidente que hay una gran diferencia entre las frecuencias de los distintos valores que toma CANT_HERIDOS_EN_SITIO_ACCIDENTE.
CANTIDAD_ACCIDENTES#
frec_cantidad = data2['CANTIDAD_ACCIDENTES'].value_counts().reset_index()
frec_cantidad.columns = ['CANTIDAD_ACCIDENTES', 'n'] #
frec_cantidad = frec_cantidad.sort_values(by='n', ascending=False)
table = go.Figure(data=[go.Table(
header=dict(values=list(frec_cantidad.columns),
fill_color='lightblue',
align='left'),
cells=dict(values=[frec_cantidad['CANTIDAD_ACCIDENTES'], frec_cantidad['n']],
fill_color='lightgrey',
align='left'))
])
table.show()
Es muy notable la significante diferencia entre los 2 valores que toma la variable CANTIDAD_ACCIDENTES.
Precio del combustible en Colombia durante el 2023#
Para este ejercicio, hay que, en primer lugar, unir todas las bases de datos para convertirlas en una sola.
d1 = pd.read_csv('C:/Users/valec/Downloads/Escritorio\precios_t12023.csv')
d2 = pd.read_csv('C:/Users/valec/Downloads/Escritorio\precios_t22023.csv')
d3 = pd.read_csv('C:/Users/valec/Downloads/Escritorio\precios_t32023.csv')
d4 = pd.read_csv('C:/Users/valec/Downloads/Escritorio\precios_t42023.csv')
Base de datos#
data23 = pd.concat([d1, d2, d3, d4], axis=0, ignore_index=True)
display(data23.head(10).style.set_caption("Base de datos: Combustible en Colombia"))
| BANDERA | NOMBRE COMERCIAL | PRODUCTO | FECHA REGISTRO | DEPARTAMENTO | MUNICIPIO | VALOR PRECIO | |
|---|---|---|---|---|---|---|---|
| 0 | TERPEL | ESTACION DE SERVICIO SERVICENTRO LA PEDRERA | DIESEL | 01-Jan-2023 | AMAZONAS | LA PEDRERA | 15000.000000 |
| 1 | TERPEL | ESTACION DE SERVICIO SERVICENTRO LA PEDRERA | GASOLINA MOTOR | 01-Jan-2023 | AMAZONAS | LA PEDRERA | 15500.000000 |
| 2 | TERPEL | BALSA EL CONDOR | GASOLINA MOTOR | 01-Jan-2023 | AMAZONAS | LETICIA | 11380.000000 |
| 3 | TERPEL | BALSA EL CONDOR | DIESEL | 01-Jan-2023 | AMAZONAS | LETICIA | 10840.000000 |
| 4 | TERPEL | ESTACION DE SERVICIO DISTRIBUIDORA LOS COMUNEROS | GASOLINA MOTOR | 01-Jan-2023 | AMAZONAS | LETICIA | 11380.000000 |
| 5 | TERPEL | ESTACION DE SERVICIO DISTRIBUIDORA LOS COMUNEROS | GASOLINA MOTOR | 01-Jan-2023 | AMAZONAS | LETICIA | 11380.000000 |
| 6 | TERPEL | ESTACION DE SERVICIO DISTRIBUIDORA LOS COMUNEROS | DIESEL | 01-Jan-2023 | AMAZONAS | LETICIA | 10671.000000 |
| 7 | TEXACO | EDS COMDECOM ABRIAQUI | GASOLINA MOTOR | 01-Jan-2023 | ANTIOQUIA | ABRIAQUÍ | 11870.000000 |
| 8 | TEXACO | EDS COMDECOM ABRIAQUI | DIESEL | 01-Jan-2023 | ANTIOQUIA | ABRIAQUÍ | 10910.000000 |
| 9 | TEXACO | ESTACIÓN DE SERVICIO Y MALL SANTA LUCIA S.A.S. | DIESEL | 01-Jan-2023 | ANTIOQUIA | AMAGÁ | 9610.000000 |
Análisis: características de la base de datos#
Esta base de datos, proporcionada por la página oficial de SICOM (sistema de información de la cadena de distribución de combustibles del Ministerio de Minas y Energía), brinda información relacionada con los precios de los combustibles distribuidos alrededor de Colombia durante el año 2023. Cada una de las columnas, ofrece información específica, como por ejemplo la empresa, el tipo de combustible y el precio de este.
En este caso, contamos con 267943 observaciones y con 7 variables. Veamos ahora, la información relacionada con el nombre y tipo de las variables.
data23.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 267943 entries, 0 to 267942
Data columns (total 7 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 BANDERA 267943 non-null object
1 NOMBRE COMERCIAL 267943 non-null object
2 PRODUCTO 267943 non-null object
3 FECHA REGISTRO 267943 non-null object
4 DEPARTAMENTO 267943 non-null object
5 MUNICIPIO 267943 non-null object
6 VALOR PRECIO 267943 non-null float64
dtypes: float64(1), object(6)
memory usage: 14.3+ MB
Podemos observar que de las 7 variables, solo 1, VALOR PRECIO, es de tipo numérica.
Análisis: variable según su tipo#
Numéricas#
VALOR PRECIO#
data23.describe()
| VALOR PRECIO | |
|---|---|
| count | 2.679430e+05 |
| mean | 1.202313e+04 |
| std | 2.870979e+04 |
| min | 0.000000e+00 |
| 25% | 9.350000e+03 |
| 50% | 1.088000e+04 |
| 75% | 1.384900e+04 |
| max | 1.475015e+07 |
sns.set_style("whitegrid")
ax = sns.boxplot(data=data23, x='VALOR PRECIO', palette=['lightblue'], orient='h')
ax.set_xlim(0, 30000)
ax.set_xlabel('Precio')
ax.set_title('Precio del combustible')
plt.grid(True, which='both', linestyle='--', linewidth=0.5)
plt.show()
La variable muestra la presencia de valores extremos significativamente alejados de la mayoría de los datos. Según el análisis realizado, los precios del combustible en cada ubicación se concentran principalmente en el rango de 9000 a 14000 pesos, aproximadamente. En consecuencia, el valor extremo superior observado, 14750147, se destaca claramente como un dato atípico en comparación con el resto de los precios registrados.
Categóricas#
BANDERA#
frec_bandera = data23['BANDERA'].value_counts().reset_index()
frec_bandera.columns = ['BANDERA', 'n']
frec_bandera = frec_bandera.sort_values(by='n', ascending=False)
table = go.Figure(data=[go.Table(
header=dict(values=list(frec_bandera.columns),
fill_color='lightblue',
align='left'),
cells=dict(values=[frec_bandera['BANDERA'], frec_bandera['n']],
fill_color='lightgrey',
align='left'))
])
table.show()
Es decir que, entre todas las comercializadoras, Terpel es la que más distribuye los diferentes tipos de combustible en el país.
PRODUCTO#
data23.value_counts('PRODUCTO')
PRODUCTO
GASOLINA MOTOR 127338
DIESEL 108205
EXTRA 32400
Name: count, dtype: int64
ax = sns.catplot(data = data23, x = 'PRODUCTO', aspect = 1.5, kind = "count", color = "lightblue")
ax.set_axis_labels('Tipo de combustible', 'Frecuencia')
ax.fig.suptitle(' Frecuencia del tipo de combustible', fontsize = 12)
sns.set_style("whitegrid")
plt.subplots_adjust(top = 0.9)
plt.grid(True, which = 'both', linestyle = '--', linewidth = 0.5)
plt.show()
Se observa que la distribución del combustible tipo Extra es considerablemente inferior en comparación con los otros dos tipos. Además, es relevante mencionar que las frecuencias de Diesel y Gasolina Motor no difieren significativamente entre sí y que, entre ambos, el combustible más comercializado en Colombia durante el año 2023 fue la Gasolina Motor, con un total de 127338 registros.
DEPARTAMENTO#
frec_dpto = data23['DEPARTAMENTO'].value_counts().reset_index()
frec_dpto.columns = ['DEPARTAMENTO', 'n']
frec_dpto = frec_dpto.sort_values(by='n', ascending=False)
table = go.Figure(data=[go.Table(
header=dict(values=list(frec_dpto.columns),
fill_color='lightblue',
align='left'),
cells=dict(values=[frec_dpto['DEPARTAMENTO'], frec_dpto['n']],
fill_color='lightgrey',
align='left'))
])
table.show()
Por lo tanto, podemos afirmar que el departamento en donde más se comercializó los 3 tipos de combustible durante el 2023 fue en Nariño, con 31054 registros.
Identificación de valores NA#
Primeramente, calculemos de manera matemática la cantidad de datos faltantes.
cantidad_na_23 = data23.isna().sum()
for columna, cantidad in cantidad_na_23.items():
print(f"{columna}: {cantidad} NA's")
BANDERA: 0 NA's
NOMBRE COMERCIAL: 0 NA's
PRODUCTO: 0 NA's
FECHA REGISTRO: 0 NA's
DEPARTAMENTO: 0 NA's
MUNICIPIO: 0 NA's
VALOR PRECIO: 0 NA's
Es decir que la base de datos no contiene ningún dato de tipo NA.
porcentaje_NA23 = data23.isna().mean() * 100
data_faltante23 = pd.DataFrame({'column': porcentaje_NA23.index, 'percent_missing': porcentaje_NA23.values})
data_faltante23 = data_faltante23.sort_values(by='percent_missing', ascending=False)
plt.figure(figsize=(10, 8))
plt.barh(data_faltante23['column'], data_faltante23['percent_missing'], color='lightblue')
plt.xlabel('Porcentaje de datos faltantes')
plt.title('Porcentaje de datos faltantes por variable')
plt.show()
La gráfica muestra que el porcentaje de valores NA para todas las variables es 0.
Detección de datos atípicos#
Hagamos uso de la herramienta boxplot para identificar la existencia de outliers en nuestra variable de interés, VALOR PRECIO, por cada tipo de combustible.
plt.figure(figsize=(12, 6))
ax = sns.boxplot(data=data23, x='PRODUCTO', y='VALOR PRECIO', palette=['lightblue'])
ax.set(ylim=(0, 30000))
ax.set_xlabel('Tipo de Producto')
ax.set_ylabel('Valor Precio')
ax.set_title('Boxplot de Precio por Tipo de Producto')
plt.grid(True, which='both', linestyle='--', linewidth=0.5)
plt.tight_layout()
plt.show()
A partir de esto, observamos que hay una evidente existencia de datos atípicos en la variable VALOR PRECIO. Por tanto, se procederá a realizar la imputación de estos.
Verifiquemos la normalidad de la variable
ks_test = stats.kstest(data23['VALOR PRECIO'], 'norm', args=(data23['VALOR PRECIO'].mean(), data23['VALOR PRECIO'].std()))
if ks_test.pvalue > 0.05:
print("La variable 'VALOR PRECIO' sigue una distribución normal.")
else:
print("La variable 'VALOR PRECIO' NO sigue una distribución normal.")
print(ks_test.pvalue)
La variable 'VALOR PRECIO' NO sigue una distribución normal.
0.0
Al hacer la prueba de Kolmogorov-Smirnov, se obtuvo un valor inferior a 0.05, esto indica que los datos no siguen una distribución normal. Es decir, la imputación debe realizarse haciendo uso de la mediana.
Q1 = data23['VALOR PRECIO'].quantile(0.25)
Q3 = data23['VALOR PRECIO'].quantile(0.75)
IQR = Q3 - Q1
lim_inf = Q1 - 1.5 * IQR
lim_sup = Q3 + 1.5 * IQR
mediana = data23['VALOR PRECIO'].median()
data23['VALOR PRECIO'] = np.where((data23['VALOR PRECIO'] < lim_inf) | (data23['VALOR PRECIO'] > lim_sup), mediana, data23['VALOR PRECIO'])
Veamos si la distribución se mantuvo
ks_test = stats.kstest(data23['VALOR PRECIO'], 'norm', args=(data23['VALOR PRECIO'].mean(), data23['VALOR PRECIO'].std()))
if ks_test.pvalue > 0.05:
print("La variable 'VALOR PRECIO' sigue una distribución normal.")
else:
print("La variable 'VALOR PRECIO' NO sigue una distribución normal.")
print(ks_test.pvalue)
La variable 'VALOR PRECIO' NO sigue una distribución normal.
0.0
La imputación fue realizada de correctamente pues la distribución de la variable VALOR PRECIO se mantuvo.
Filtración de la base de datos#
Para este ejercicio, vamos a filtrar la base según un solo tipo de combustible: Gasolina Motor y luego procederemos a analizarla con respecto a las diferentes columnas.
data_23_1 = data23[data23['PRODUCTO'] == 'GASOLINA MOTOR']
display(data_23_1.head(10).style.set_caption("Base de datos: Combustible de tipo Gasolina Motor"))
| BANDERA | NOMBRE COMERCIAL | PRODUCTO | FECHA REGISTRO | DEPARTAMENTO | MUNICIPIO | VALOR PRECIO | |
|---|---|---|---|---|---|---|---|
| 1 | TERPEL | ESTACION DE SERVICIO SERVICENTRO LA PEDRERA | GASOLINA MOTOR | 01-Jan-2023 | AMAZONAS | LA PEDRERA | 15500.000000 |
| 2 | TERPEL | BALSA EL CONDOR | GASOLINA MOTOR | 01-Jan-2023 | AMAZONAS | LETICIA | 11380.000000 |
| 4 | TERPEL | ESTACION DE SERVICIO DISTRIBUIDORA LOS COMUNEROS | GASOLINA MOTOR | 01-Jan-2023 | AMAZONAS | LETICIA | 11380.000000 |
| 5 | TERPEL | ESTACION DE SERVICIO DISTRIBUIDORA LOS COMUNEROS | GASOLINA MOTOR | 01-Jan-2023 | AMAZONAS | LETICIA | 11380.000000 |
| 7 | TEXACO | EDS COMDECOM ABRIAQUI | GASOLINA MOTOR | 01-Jan-2023 | ANTIOQUIA | ABRIAQUÍ | 11870.000000 |
| 10 | TEXACO | ESTACIÓN DE SERVICIO Y MALL SANTA LUCIA S.A.S. | GASOLINA MOTOR | 01-Jan-2023 | ANTIOQUIA | AMAGÁ | 11200.000000 |
| 11 | TERPEL | ESTACION DE SERVICIO PUERTAS DEL NORDESTE | GASOLINA MOTOR | 01-Jan-2023 | ANTIOQUIA | BARBOSA | 10560.000000 |
| 14 | TERPEL | ESTACION DE SERVICIO POPALITO | GASOLINA MOTOR | 01-Jan-2023 | ANTIOQUIA | BARBOSA | 10399.000000 |
| 17 | PRIMAX | ESTACION DE SERVICIO PRIMAX AUTOPISTA | GASOLINA MOTOR | 01-Jan-2023 | ANTIOQUIA | BELLO | 10300.000000 |
| 18 | TERPEL | TERPEL FONTIDUENO | GASOLINA MOTOR | 01-Jan-2023 | ANTIOQUIA | BELLO | 10870.000000 |
Debido a que existen varios tipos de valores en BANDERA, vamos a analizarlo con los 4 de mayor frecuencia encontrados con la base original, con el fin de observar si hubo algún cambio en el comportimiento. Estos son Terpel, Primax, Biomax y Texaco.
empresas = ["TERPEL", "PRIMAX", "BIOMAX", "TEXACO"]
empresa = data_23_1[data_23_1['BANDERA'].isin(empresas)]
frec_empresa = pd.crosstab(empresa['BANDERA'], empresa['PRODUCTO'])
ax = frec_empresa.plot(kind='bar', figsize=(10, 6), color='lightblue', edgecolor='black', width=0.8, legend=False)
plt.title("Frecuencia de empresas por Gasolina Motor")
plt.xlabel("Empresa")
plt.ylabel("Frecuencia")
plt.xticks(rotation=0)
plt.tight_layout()
plt.show()
Es posible observar que, una vez más, la bandera Terpel es la que más comercializó combustible, en este caso de tipo Gasolina Motor, mientras que la que menos lo hizo fue Texaco. Por otro lado, se evidencia un cambio en el comportamiento entre Biomax y Primax pues para este producto, Biomax presenta una mayor frecuencia de comercialización. Sin embargo, al analizar los tres tipos de combustibles conjuntamente, Primax es la que se distribuyó con mayor frecuencia entre ambas empresas.
Realizaremos esto mismo con los 4 valores de DEPARTAMENTO con mayor frecuencia.
departamentos = ["NARIÑO", "ANTIOQUIA", "NORTE DE SANTANDER", "VALLE DEL CAUCA"]
departamento = data_23_1[data_23_1['DEPARTAMENTO'].isin(departamentos)]
frec_dpto = pd.crosstab(departamento['DEPARTAMENTO'], departamento['PRODUCTO'])
ax = frec_dpto.plot(kind='bar', figsize=(10, 6), color='lightblue', edgecolor='black', width=0.8, legend=False)
plt.title("Frecuencia de departamentos por Gasolina Motor")
plt.xlabel("Departamento")
plt.ylabel("Frecuencia")
plt.xticks(rotation=0)
plt.tight_layout()
plt.show()
Observamos que este subconjunto tiene el mismo comportamiento de la base de datos original para estos 4 departamentos.
Ahora, veamos cómo se comporta el precio de este tipo de combustible por departamento.
result = (data_23_1.groupby('DEPARTAMENTO')
.agg(precio_promedio=('VALOR PRECIO', 'mean'))
.reset_index()
.sort_values(by='precio_promedio'))
# Crear la tabla con plotly
table = go.Figure(data=[go.Table(
header=dict(values=list(result.columns),
fill_color='lightblue',
align='left'),
cells=dict(values=[result[col] for col in result.columns],
fill_color='lightgrey',
align='left'))
])
table.update_layout(title='Precio Promedio por Departamento')
table.show()
Creemos un rango para definir el nivel en el que se encuentra el promedio, esto servirá más adelante para realizar el mapa.
bins = [10000, 12000, 14000, 16000]
labels = ["Bajo", "Medio", "Alto"]
result['rango_precio'] = pd.cut(result['precio_promedio'], bins=bins, labels=labels, right=False)
# Ordenar por el rango_precio
rangos = result.sort_values(by='rango_precio')
table = go.Figure(data=[go.Table(
header=dict(values=list(rangos.columns),
fill_color='lightblue',
align='left'),
cells=dict(values=[result[col] for col in result.columns],
fill_color='lightgrey',
align='left'))
])
table.update_layout(title='Precio Promedio por Departamento')
table.show()
Se puede concluir que la mayoría de departamentos se encuentra en el rango establecido Medio, pues de los 32, solo NARIÑO y CESAR son de tipo BAJO, y en el caso de tipo ALTO solo encontramos a SAN ANDRES, GUAVIARE, VAUPES, GUAINIA y AMAZONAS.
Mapa de geolocalización#
Prcoderemos a graficar con la ayuda de un mapa los rangos creados en la sección anterior, con el fin de visualizar la distribución de precio promedio del combustible en Colombia.
Primeramente, vamos a leer el archivo Shapefile de Colombia y haremos un ajuste de nombre para el departamento de Nariño para evitar posibles errores.
mapa_col = gpd.read_file("C:/Users/valec/Downloads/Python _Act/Python _Act/coordenadas/COLOMBIA/COLOMBIA.shp")
mapa_col.loc[mapa_col['DPTO_CNMBR'] == 'NARI?O', 'DPTO_CNMBR'] = 'NARIÑO'
Ahora, vamos a unir la base que contiene a los rangos junto con la del archivo de coordenadas.
data_precios = data_23_rangos.groupby('DEPARTAMENTO')['VALOR PRECIO'].mean().reset_index()
data_precios['VALOR PRECIO'] = pd.to_numeric(df_precios['VALOR PRECIO'], errors='coerce')
# Unir los datos geoespaciales con precios
mapa_col = mapa_col.merge(data_precios[['DEPARTAMENTO', 'VALOR PRECIO']],
left_on='DPTO_CNMBR',
right_on='DEPARTAMENTO',
how='left')
bins = [10000, 12000, 14000, 16000]
labels = ["Bajo", "Medio", "Alto"]
mapa_col['rango_precio'] = pd.cut(mapa_col['VALOR PRECIO'], bins=bins, labels=labels, right=False)
Y ahora, haciendo uso de la librería folium, vamos a graficar el mapa.
m = folium.Map(location=[4.5709, -74.2973], zoom_start=6) # Centro de Colombia
color_map = {
'Bajo': 'lightgreen',
'Medio': 'lightyellow',
'Alto': 'lightcoral'
}
for _, row in mapa_col.iterrows():
folium.GeoJson(
row['geometry'],
style_function=lambda feature, color=color_map.get(row['rango_precio']): {
'fillColor': color,
'color': 'black',
'weight': 0.5,
'fillOpacity': 0.7
},
tooltip=folium.Tooltip(f"{row['DEPARTAMENTO']}: {row['rango_precio']}")
).add_to(m)
m.get_root().html.add_child(folium.Element(legend_html))
display(m)